import errno
import os

from middlewared.service_exception import CallError
from middlewared.utils.path import FSLocation, path_location, strip_location_prefix
from middlewared.async_validators import check_path_resides_within_volume

from .crud_service import CRUDService
from .decorators import pass_app, private


class SharingTaskService(CRUDService):

    path_field = 'path'
    allowed_path_types = [FSLocation.LOCAL]
    enabled_field = 'enabled'
    locked_field = 'locked'
    locked_alert_class = NotImplemented
    share_task_type = NotImplemented

    @private
    async def get_path_field(self, data):
        return data[self.path_field]

    @private
    async def sharing_task_extend_context(self, rows, extra):
        if extra.get('use_cached_locked_datasets', True):
            locked_ds_endpoint = 'pool.dataset.locked_datasets_cached'
        else:
            locked_ds_endpoint = 'zfs.dataset.locked_datasets'

        if extra.get('select'):
            select_fields = []
            for entry in extra['select']:
                if isinstance(entry, list) and entry:
                    select_fields.append(entry[0])
                elif isinstance(entry, str):
                    # Just being extra sure so that we don't crash
                    select_fields.append(entry)

            if self.locked_field not in select_fields:
                extra['retrieve_locked_info'] = False

        return {
            'locked_datasets': await self.middleware.call(
                locked_ds_endpoint
            ) if extra.get('retrieve_locked_info', True) else [],
            'service_extend': (
                await self.middleware.call(self._config.datastore_extend_context, rows, extra)
                if self._config.datastore_extend_context else None
            ),
            'retrieve_locked_info': extra.get('retrieve_locked_info', True),
        }

    @private
    async def validate_cluster_path(self, verrors, name, volname, path):
        if volname not in await self.middleware.call('gluster.volume.list'):
            verrors.add(name, f'{volname}: cluster volume does not exist.')
            return

        try:
            await self.middleware.call('filesystem.stat', f'CLUSTER:{volname}{path}')
        except CallError as e:
            if e.errno is errno.ENXIO:
                verrors.add(name, f'{volname}: cluster volume is not mounted.')
            elif e.errno is errno.ENOENT:
                # this is not treated as fatal error in `check_path_resides_within_volume`
                # but the design decision may need further review
                pass
            else:
                raise

    @private
    async def validate_external_path(self, verrors, name, path):
        # Services with external paths must implement their own
        # validation here because we can't predict what is required.
        raise NotImplementedError

    @private
    async def validate_local_path(self, verrors, name, path):
        await check_path_resides_within_volume(verrors, self.middleware, name, path)

    @private
    async def validate_path_field(self, data, schema, verrors):
        name = f'{schema}.{self.path_field}'
        path = await self.get_path_field(data)
        loc = path_location(path)

        if loc not in self.allowed_path_types:
            verrors.add(name, f'{loc.name}: path type is not allowed.')

        elif loc is FSLocation.CLUSTER:
            try:
                volname, volpath = strip_location_prefix(path).split('/', 1)
            except ValueError:
                verrors.add(name, f'{path}: path within cluster volume must be specified.')
            else:
                volpath = os.path.join('/', volpath)
                await self.validate_cluster_path(verrors, name, volname, volpath)

        elif loc is FSLocation.EXTERNAL:
            await self.validate_external_path(verrors, name, strip_location_prefix(path))

        elif loc is FSLocation.LOCAL:
            await self.validate_local_path(verrors, name, path)

        else:
            self.logger.error('%s: unknown location type', loc.name)
            raise NotImplementedError

        return verrors

    @private
    async def sharing_task_determine_locked(self, data, locked_datasets):
        path = await self.get_path_field(data)
        if path_location(path) is not FSLocation.LOCAL:
            return False

        return await self.middleware.call(
            'pool.dataset.path_in_locked_datasets', path, locked_datasets
        )

    @private
    async def sharing_task_extend(self, data, context):
        args = [data] + ([context['service_extend']] if self._config.datastore_extend_context else [])

        if self._config.datastore_extend:
            data = await self.middleware.call(self._config.datastore_extend, *args)

        if context['retrieve_locked_info']:
            data[self.locked_field] = await self.middleware.call(
                f'{self._config.namespace}.sharing_task_determine_locked', data, context['locked_datasets']
            )

        return data

    @private
    async def get_options(self, options):
        return {
            **(await super().get_options(options)),
            'extend': f'{self._config.namespace}.sharing_task_extend',
            'extend_context': f'{self._config.namespace}.sharing_task_extend_context',
        }

    @private
    async def human_identifier(self, share_task):
        raise NotImplementedError

    @private
    async def generate_locked_alert(self, share_task_id):
        share_task = await self.get_instance(share_task_id)
        await self.middleware.call(
            'alert.oneshot_create', self.locked_alert_class,
            {**share_task, 'identifier': await self.human_identifier(share_task), 'type': self.share_task_type}
        )

    @private
    async def remove_locked_alert(self, share_task_id):
        await self.middleware.call(
            'alert.oneshot_delete', self.locked_alert_class, f'"{self.share_task_type}_{share_task_id}"'
        )

    @pass_app(rest=True)
    async def update(self, app, id_, data):
        rv = await super().update(app, id_, data)
        if not rv[self.enabled_field] or not rv[self.locked_field]:
            await self.remove_locked_alert(rv['id'])
        return rv

    @pass_app(rest=True)
    async def delete(self, app, id_, *args):
        rv = await super().delete(app, id_, *args)
        await self.remove_locked_alert(id_)
        return rv


class SharingService(SharingTaskService):
    locked_alert_class = 'ShareLocked'

    @private
    async def human_identifier(self, share_task):
        return share_task['name']


class TaskPathService(SharingTaskService):
    locked_alert_class = 'TaskLocked'

    @private
    async def human_identifier(self, share_task):
        return await self.get_path_field(share_task)
